#!/usr/bin/env python

"""
A go board display, for Gtk+, using Cairo.

Ken Harris <kengruven@gmail.com>
2006.1.12

----

FIXME: use Point for all points, not x,y tuples

NEXT:
- hook up events so i can play against myself (use rules.py, or whatever) -- board backing?

- write a _draw_stone(pt,color), maybe, that draws a single stone; use this for cursors, as well

OPTIONS:
- use_bitmaps (2: stones, wood?), show_coords, one_color, use_shadows?, use_jitter
- how these work: they're just gconf keys (bools, mostly; bitmaps are filenames -- rel?)
- (shouldn't be bitmaps-only: should allow svg, as well)

TODO:
- handle events:
-x- mouse-over (incl. enter/leave)
--- click: make this down-and-up in the same cell!  (like a button)
--- right-click (popup)
--- drag?  (future: for drawing lines, etc.)
--- keypresses?
- bitmaps: use multiple white-stone bitmaps
- handle sgf drawing commands: labels, symbols, lines, ...
- (all of these options will go in the prefs, under 'Graphics')

FUTURE:
- extract the cairo part of this, so it's usable on non-Gtk platforms?
--- yep, e.g., <http://cairographics.org/pycairo> -- we'll want to be able to save PNG, SVG, PDF, ...
- scale text, rather than using different point sizes?
- when you make it really tiny (<10px?), it flips!  in cairo routine: if size is smaller than ???, don't draw anything?
- no line cap looks quite right; fix by drawing the outer square in one go, then the inner lines with cap_butt
- lettering shouldn't be the same size as a square: it should max out at ???
- use stones.py (drawStone.c port), and generate my own unique white stones on startup -- at high res (64?)

MORE:
- center text correctly (what's wrong?)
- extract pt<->x,y functions
- extract cairoboard from gtkboard
- split draw() into draw_shadows(), draw_stones(), etc.
- add way to override gconf, e.g., for thumbnailing
- handle non-square boards
"""

import gtk
import math
import cairo
import gconf

from point import Point

from board import BLACK, WHITE

# FUTURE: load these once, when needed -- BETTER: use stones.py!
black_bitmap = cairo.ImageSurface.create_from_png("Resources/cgoban-black-stone.png")
white_bitmap = cairo.ImageSurface.create_from_png("Resources/cgoban-white-stone.png")
wood_bitmap = cairo.ImageSurface.create_from_png("Resources/cgoban-wood.png")

# TODO:
# - extract cairoboard.py as just draw() (plus x,y<->pt fn's)
# - rename this gtkboard.py/GTKBoard, and have it just call cairoboard.py (and deal with events)
# - write a printboard.py that uses cairoboard.py to print...

class CairoBoard(gtk.DrawingArea):
    def __init__(self, game):
        gtk.DrawingArea.__init__(self)
        self.connect("expose_event", self.expose)

        self.game = game

        # minimum size
        self.set_size_request(50, 50)

        client = gconf.client_get_default()
        client.add_dir('/apps/gimme/display', gconf.CLIENT_PRELOAD_NONE)
        client.notify_add('/apps/gimme/display', self.refresh_from_gconf)

        game.connect("cursor_moved", self.on_cursor_moved)

    def on_cursor_moved(self, game, cursor):
        __pychecker__ = 'unusednames=game,cursor'
        self.refresh()

    def refresh_from_gconf(self, client, cnxn_id, entry, userdata):
        __pychecker__ = 'unusednames=client,cnxn_id,entry,userdata'
        self.refresh()

    def refresh(self, pt=None):
        """refresh the point |pt|.  if no point given, refresh everything."""
        if pt == None:
            self.queue_draw()
            return

        # we refresh for 1/2 a space more on each side of the square
        # itself, to account for shadows'n'stuff.

        ## rect = self.get_allocation() -- this is the size of the widget
        print "WRITEME: compute x,y,width,height"
        self.queue_draw_area(x, y, width, height)

    def expose(self, widget, event):
        self.context = widget.window.cairo_create()
        
        # try this:
##        self.context.set_line_cap(cairo.LINE_CAP_SQUARE)

        # set a clip region for the expose event
        self.context.rectangle(event.area.x, event.area.y,
                               event.area.width, event.area.height)
        self.context.clip()
        
        self.draw(self.context)
        
        return False
    
    def draw(self, context):
        board = self.game.cursor.board # NEW!

        rows = cols = board.size # LIMITATION: Board class does square boards only, for now

        rect = self.get_allocation()
        
        use_labels = gconf.client_get_default().get_bool("/apps/gimme/display/coordinates")
        use_bitmaps = gconf.client_get_default().get_bool("/apps/gimme/display/textures")
        
        if use_labels:
            r = min((rect.width-10) / ((cols+1)*2.0), (rect.height-10) / ((rows+1)*2.0))
        else:
            r = min((rect.width-10) / ((cols)*2.0), (rect.height-10) / ((rows)*2.0))
        d = 2*r

        # origin: -- use this below for every loop!
        if use_labels:
            x0 = (rect.width - d*(cols+1))/2 + d
            y0 = rect.height - (rect.height - d*(rows+1))/2 - d
        else:
            x0 = (rect.width - d*(cols))/2
            y0 = rect.height - (rect.height - d*(rows))/2

        # the board
        if use_bitmaps:
            w = wood_bitmap.get_width()
            h = wood_bitmap.get_height()

            old = context.get_matrix()

            # stamp a copy on the left half
            context.translate(x0 + 1, y0-d*rows) # +1, to be sure they touch
            context.scale(cols*d/float(2*w), rows*d/float(h))
            context.set_source_surface(wood_bitmap)
            context.paint()

            context.set_matrix(old)

            # stamp another copy, mirrored, on the right half
            context.translate(x0 + d*cols/2.0, y0-d*rows)
            context.translate(d*cols/2.0 - 1, 0) # -1, to be sure they touch
            context.scale(-cols*d/float(2*w), rows*d/float(h))
            context.set_source_surface(wood_bitmap)
            context.paint()

            context.set_matrix(old)
        else:
            context.set_source_rgb(0.9, 0.6, 0.4)
            context.rectangle(x0, y0-d*rows, cols*d, rows*d)
            context.fill()

        # the grid
        context.set_source_rgb(0, 0, 0)
        context.set_line_width(r / 20.0)
        for i in xrange(rows):
            # across
            context.move_to(x0+r, y0 - r - d*i)
            context.line_to(x0+r+d*(cols-1), y0 - r - d*i)
        for i in xrange(cols):
            # down
            context.move_to(x0 + d*i + r, y0 - r)
            context.line_to(x0 + d*i + r, y0 - r - d*(rows-1))
        context.stroke()

        # hoshi
        context.set_source_rgb(0, 0, 0)
        from handicap import get_handicap, get_max_handicap
        for pt in get_handicap(board.size, get_max_handicap(board.size)):
            context.arc(x0 + r + pt.col*d, y0 - r - pt.row*d, r*0.15, 0.0, 2*math.pi)
            context.fill()

        # HACK: build |stones| dict from board -- WHY?  DOES THIS REALLY HELP?
        stones = {}
        for x in xrange(cols):
            for y in xrange(rows):
                pt = Point(x,y)
                value = board.get(pt)
                if value == WHITE:
                    stones[pt] = WHITE
                elif value == BLACK:
                    stones[pt] = BLACK

        # shadows
        for pt in stones:
            x,y = pt.col, pt.row

            # center, in cairo coordinates -- EXTRACT!
            cx = x0 + x*d + r
            cy = y0 - y*d - r

            cy += r/6 # offset it a bit
            cx += r/6

            # cx0,cy0,r0, cx1,cy1,r1
            pat = cairo.RadialGradient(cx, cy, 0.0,
                                       cx, cy, r*1.1)
            pat.add_color_stop_rgba(0.5,  0,0,0, 1) # 0.5=black
            pat.add_color_stop_rgba(1.0,  0,0,0, 0) # 1.0=transparent
            context.set_source(pat)
            context.arc(cx, cy, r*1.1, 0.0, 2*math.pi)
            context.fill()

        # stones
        for pt in stones:
            if stones[pt] == BLACK:
                context.set_source_rgb(0, 0, 0)
            else:
                context.set_source_rgb(1, 1, 1)
            x,y = pt.col, pt.row

            # center, in cairo coordinates
            cx = x0 + x*d + r
            cy = y0 - y*d - r

            if use_bitmaps:
                if stones[pt] == BLACK:
                    stone_bitmap = black_bitmap
                else:
                    stone_bitmap = white_bitmap
                w = stone_bitmap.get_width()
                h = stone_bitmap.get_height()

                old = context.get_matrix()

                context.translate(cx - r, cy - r)
                context.scale(d*0.97/w, d*0.97/h)
                context.set_source_surface(stone_bitmap)
                context.paint()

                context.set_matrix(old)
            else:
                context.arc(cx, cy, r*0.97, 0.0, 2*math.pi)
                context.fill()

        # markers
        from board import MARKER_BLACK_TERRITORY, MARKER_WHITE_TERRITORY, \
                          MARKER_TRIANGLE, MARKER_SQUARE, MARKER_X, MARKER_CIRCLE, MARKER_SELECTED
        context.set_line_width(r / 8.0) # all markers have the same line-width
        for (point,mark) in board.markers:
            # center, in cairo coordinates
            cx = x0 + point.col*d + r
            cy = y0 - point.row*d - r

            # set color -- opposite of the stone's color, else black
            opposite_color = {BLACK: (1.0, 1.0, 1.0),
                              WHITE: (0.0, 0.0, 0.0),
                              0: (0.0, 0.0, 0.0)}[board.get(point)] # TODO: board[pt] sure would be nice!

            if mark in [MARKER_BLACK_TERRITORY, MARKER_WHITE_TERRITORY]:
                if mark == MARKER_WHITE_TERRITORY:
                    context.set_source_rgba(1, 1, 1, 0.5)
                else:
                    context.set_source_rgba(0, 0, 0, 0.5)
                context.arc(cx, cy, r*0.5, 0.0, 2*math.pi)
                context.fill()
            elif mark == MARKER_TRIANGLE:
                context.set_source_rgb(*opposite_color)

                # TODO: make triangle equilateral -- also, all shapes the same size!
                context.move_to(cx - r/2., cy + r/2.)
                context.line_to(cx + r/2., cy + r/2.)
                context.line_to(cx, cy - r/2)
                context.close_path()
                context.stroke()
            elif mark == MARKER_SQUARE:
                context.set_source_rgb(*opposite_color)

                context.move_to(cx - r/2., cy + r/2.)
                context.line_to(cx + r/2., cy + r/2.)
                context.line_to(cx + r/2., cy - r/2.)
                context.line_to(cx - r/2., cy - r/2.)
                context.close_path()
                context.stroke()
            elif mark == MARKER_X:
                context.set_source_rgb(*opposite_color)

                context.move_to(cx - r/2., cy + r/2.)
                context.line_to(cx + r/2., cy - r/2.)
                context.move_to(cx + r/2., cy + r/2.)
                context.line_to(cx - r/2., cy - r/2.)
                context.stroke()
            elif mark == MARKER_CIRCLE:
                context.set_source_rgb(*opposite_color)

                context.arc(cx, cy, r*0.6, 0.0, 2*math.pi)
                context.stroke()
            elif mark == MARKER_SELECTED:
                context.set_source_rgb(0.0, 0.0, 1.0) # blue -- SGF FF[4] screenshot shows this

                context.move_to(cx - r/3., cy + r/3.)
                context.line_to(cx + r/3., cy + r/3.)
                context.line_to(cx + r/3., cy - r/3.)
                context.line_to(cx - r/3., cy - r/3.)
                context.close_path()
                context.fill()
            elif isinstance(mark, basestring):
                context.set_source_rgb(*opposite_color)

                # measure it
                (x_bearing, y_bearing, width, height, x_advance, y_advance) = context.text_extents(mark)
                real_width = width # save width here

                # measure 'X' (for height)
                (x_bearing, y_bearing, width, height, x_advance, y_advance) = context.text_extents('X')
                real_height = height # save height here


                # TODO: if there's no stone here, clear the lines from this cell

                # move, set size, and write it
                context.set_font_size(r)
                context.move_to(cx - real_width/2.0, cy + real_height/2.0)
                context.show_text(mark)
                context.stroke()
                # BUG: spacing is funny!  why?
            else:
                print "WRITEME: use logging, and log an error here!"

        # last-played-stone marker -- TODO: merge this with the code to draw other markers
        if board.last_stone:
            # show it
            if board.turn == BLACK:
                color = (0, 0, 0)
            else:
                color = (1, 1, 1)

            # center, in cairo coordinates
            point = board.last_stone
            cx = x0 + point.col*d + r
            cy = y0 - point.row*d - r
            
            context.set_source_rgb(*color)
            context.set_line_width(r / 8.0)
            context.arc(cx, cy, r*0.6, 0.0, 2*math.pi)
            context.stroke()

        # cursor-stone
##        if self.cursor: # FUTURE: only if you can place something here!
##            x,y = self.cursor.col, self.cursor.row # no longer needed...

##            context.set_source_rgba(1, 1, 1, 0.5) # FUTURE: correct color!
##            context.arc(x0 + (1+2*x+2)*r, y0 - (3+2*y)*r, r*0.97, 0.0, 2*math.pi)
##            context.fill()

        # BUG: labels should be drawn with gridlines -- they're at the same z-level

        if use_labels:
            # labels
            context.set_source_rgb(0, 0, 0)
            context.set_font_size(r)
            for i in xrange(rows):
                # up the side

                # number to draw
                char = "%d" % (i+1)

                # measure it
                (x_bearing, y_bearing, width, height, x_advance, y_advance) = context.text_extents(char)

                # move, and write it
                context.move_to(x0 - r - width/2.0, y0 - d*(i+1-0.5) + height/2.0)
                context.show_text(char)

            for i in xrange(cols):
                # across the bottom

                # letter to draw -- EXTRACT IN point.py: functions for converting between int<->char
                if i < ord('I')-ord('A'):
                    char = "%c" % (ord('A')+i)
                elif i <= ord('Z') - ord('A') - 1:
                    char = "%c" % (ord('A')+i + 1)
                else:
                    char = "%c%c" % ((ord('A')+i + 1 - (ord('Z')-ord('A')+1),) * 2) # double letters: IS THIS RIGHT?
                    # BUG: this doesn't skip 'II'!
                assert cols <= 50, "I don't know how to label boards that big yet!"
                # TODO: computing dots doesn't work beyond 19x19!

                # measure it
                (x_bearing, y_bearing, width, height, x_advance, y_advance) = context.text_extents(char)

                # move, and write it
                context.move_to(x0 + d*(i+1-0.5) - width/2.0, y0 + r + height/2.0)
                context.show_text(char)

old_notes_from_gtk_board = """
STRATEGY:
- other things to provide:
--- all sgf drawing commands
--- a way to do highlight-mouseovers
--- a way to provide a popup menu (!)
--- a way to specify the move: user's, or not-user's
--- a way to set points on the board!  either a Board backing, or set(pt,val) method?

EVENTS:
- click
- mouse-on, mouse-off
- right-click (popup)
- dragging?
- press-key?  (do i do that here?)  (e.g., hold down shift to see...)

TODO:
- add events: click, mouse-on, mouse-off, menu; they supply the Point of the click (or None)
--- what event framework?  my own?  just callbacks?  provide methods to override?
- set a decent default size
- add methods to support sgf-style drawing -- either new methods, or add these to Board?
"""
